/***************************************************************************

Copyright (c) Microsoft Corporation. All rights reserved.
This code is licensed under the Visual Studio SDK license terms.
THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

***************************************************************************/

using System;
using System.Diagnostics;
using System.Globalization;
using System.CodeDom.Compiler;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.Win32;
using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider;

namespace Microsoft.VisualStudio.Package
{

	/// <summary>
	/// This class implements an MSBuild logger that output events to VS outputwindow and tasklist.
	/// </summary>
	[ComVisible(true)]
	internal sealed class IDEBuildLogger : Logger
	{
		#region fields
		// TODO: Remove these constants when we have a version that suppoerts getting the verbosity using automation.
		private string buildVerbosityRegistryRoot = @"Software\Microsoft\VisualStudio\9.0";
		private const string buildVerbosityRegistrySubKey = @"General";
		private const string buildVerbosityRegistryKey = "MSBuildLoggerVerbosity";
		// TODO: Re-enable this constants when we have a version that suppoerts getting the verbosity using automation.
		//private const string EnvironmentCategory = "Environment";
		//private const string ProjectsAndSolutionSubCategory = "ProjectsAndSolution";
		//private const string BuildAndRunPage = "BuildAndRun";

		private int currentIndent;
		private IVsOutputWindowPane outputWindowPane;
		private string errorString = SR.GetString(SR.Error, CultureInfo.CurrentUICulture);
		private string warningString = SR.GetString(SR.Warning, CultureInfo.CurrentUICulture);
		private bool isLogTaskDone;
		private TaskProvider taskProvider;
		private IVsHierarchy hierarchy;
		private IServiceProvider serviceProvider;

		#endregion

		#region properties
		public string WarningString
		{
			get { return this.warningString; }
			set { this.warningString = value; }
		}
		public string ErrorString
		{
			get { return this.errorString; }
			set { this.errorString = value; }
		}
		public bool IsLogTaskDone
		{
			get { return this.isLogTaskDone; }
			set { this.isLogTaskDone = value; }
		}
		/// <summary>
		/// When building from within VS, setting this will
		/// enable the logger to retrive the verbosity from
		/// the correct registry hive.
		/// </summary>
		internal string BuildVerbosityRegistryRoot
		{
			get { return buildVerbosityRegistryRoot; }
			set { buildVerbosityRegistryRoot = value; }
		}
		/// <summary>
		/// Set to null to avoid writing to the output window
		/// </summary>
		internal IVsOutputWindowPane OutputWindowPane
		{
			get { return outputWindowPane; }
			set { outputWindowPane = value; }
		}
		#endregion

		#region ctors
		/// <summary>
		/// Constructor.  Inititialize member data.
		/// </summary>
		public IDEBuildLogger(IVsOutputWindowPane output, TaskProvider taskProvider, IVsHierarchy hierarchy)
		{
			if (taskProvider == null)
				throw new ArgumentNullException("taskProvider");
			if (hierarchy == null)
				throw new ArgumentNullException("hierarchy");

			this.taskProvider = taskProvider;
			this.outputWindowPane = output;
			this.hierarchy = hierarchy;
			IOleServiceProvider site;
			Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(hierarchy.GetSite(out site));
			this.serviceProvider = new ServiceProvider(site);
		}
		#endregion

		#region overridden methods
		/// <summary>
		/// Overridden from the Logger class.
		/// </summary>
		public override void Initialize(IEventSource eventSource)
		{
			if (null == eventSource)
			{
				throw new ArgumentNullException("eventSource");
			}
			eventSource.BuildStarted += new BuildStartedEventHandler(BuildStartedHandler);
			eventSource.BuildFinished += new BuildFinishedEventHandler(BuildFinishedHandler);
			eventSource.ProjectStarted += new ProjectStartedEventHandler(ProjectStartedHandler);
			eventSource.ProjectFinished += new ProjectFinishedEventHandler(ProjectFinishedHandler);
			eventSource.TargetStarted += new TargetStartedEventHandler(TargetStartedHandler);
			eventSource.TargetFinished += new TargetFinishedEventHandler(TargetFinishedHandler);
			eventSource.TaskStarted += new TaskStartedEventHandler(TaskStartedHandler);
			eventSource.TaskFinished += new TaskFinishedEventHandler(TaskFinishedHandler);
			eventSource.CustomEventRaised += new CustomBuildEventHandler(CustomHandler);
			eventSource.ErrorRaised += new BuildErrorEventHandler(ErrorHandler);
			eventSource.WarningRaised += new BuildWarningEventHandler(WarningHandler);
			eventSource.MessageRaised += new BuildMessageEventHandler(MessageHandler);
		}
		#endregion

		#region event delegates
		/// <summary>
		/// This is the delegate for error events.
		/// </summary>
		private void ErrorHandler(object sender, BuildErrorEventArgs errorEvent)
		{
			AddToErrorList(
				errorEvent,
				errorEvent.Code,
				errorEvent.File,
				errorEvent.LineNumber,
				errorEvent.ColumnNumber);
		}

		/// <summary>
		/// This is the delegate for warning events.
		/// </summary>
		private void WarningHandler(object sender, BuildWarningEventArgs errorEvent)
		{
			AddToErrorList(
				errorEvent,
				errorEvent.Code,
				errorEvent.File,
				errorEvent.LineNumber,
				errorEvent.ColumnNumber);
		}

		/// <summary>
		/// Add the error/warning to the error list and potentially to the output window.
		/// </summary>
		private void AddToErrorList(
			BuildEventArgs errorEvent,
			string errorCode,
			string file,
			int line,
			int column)
		{
			TaskPriority priority = (errorEvent is BuildErrorEventArgs) ? TaskPriority.High : TaskPriority.Normal;
			if (OutputWindowPane != null
				&& (this.Verbosity != LoggerVerbosity.Quiet || errorEvent is BuildErrorEventArgs))
			{
				// Format error and output it to the output window
				string message = this.FormatMessage(errorEvent.Message);
				CompilerError e = new CompilerError(file,
													line,
													column,
													errorCode,
													message);
				e.IsWarning = (errorEvent is BuildWarningEventArgs);

				Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(OutputWindowPane.OutputStringThreadSafe(GetFormattedErrorMessage(e)));
			}

			// Add error to task list
			ErrorTask task = new ErrorTask();
			task.Document = file;
			task.Line = line - 1; // The task list does +1 before showing this number.
			task.Column = column;
			task.Text = errorEvent.Message;
			task.Priority = priority;
			task.Category = TaskCategory.BuildCompile;
			task.HierarchyItem = hierarchy;
			task.Navigate += new EventHandler(NavigateTo);
			if (errorEvent is BuildWarningEventArgs)
				task.ErrorCategory = TaskErrorCategory.Warning;
			this.taskProvider.Tasks.Add(task);
		}


		/// <summary>
		/// This is the delegate for Message event types
		/// </summary>		
		private void MessageHandler(object sender, BuildMessageEventArgs messageEvent)
		{
			if (LogAtImportance(messageEvent.Importance))
			{
				LogEvent(sender, messageEvent);
			}
		}

		private void NavigateTo(object sender, EventArgs arguments)
		{
			Microsoft.VisualStudio.Shell.Task task = sender as Microsoft.VisualStudio.Shell.Task;
			if (task == null)
				throw new ArgumentException("sender");

			// Get the doc data for the task's document
			if (String.IsNullOrEmpty(task.Document))
				return;

			IVsUIShellOpenDocument openDoc = serviceProvider.GetService(typeof(IVsUIShellOpenDocument)) as IVsUIShellOpenDocument;
			if (openDoc == null)
				return;

			IVsWindowFrame frame;
			IOleServiceProvider sp;
			IVsUIHierarchy hier;
			uint itemid;
			Guid logicalView = VSConstants.LOGVIEWID_Code;

            if (Microsoft.VisualStudio.ErrorHandler.Failed(openDoc.OpenDocumentViaProject(task.Document, ref logicalView, out sp, out hier, out itemid, out frame)) || frame == null)
				return;

			object docData;
			frame.GetProperty((int)__VSFPROPID.VSFPROPID_DocData, out docData);

			// Get the VsTextBuffer
			VsTextBuffer buffer = docData as VsTextBuffer;
			if (buffer == null)
			{
				IVsTextBufferProvider bufferProvider = docData as IVsTextBufferProvider;
				if (bufferProvider != null)
				{
					IVsTextLines lines;
                    Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(bufferProvider.GetTextBuffer(out lines));
					buffer = lines as VsTextBuffer;
					Debug.Assert(buffer != null, "IVsTextLines does not implement IVsTextBuffer");
					if (buffer == null)
						return;
				}
			}

			// Finally, perform the navigation.
			IVsTextManager mgr = serviceProvider.GetService(typeof(VsTextManagerClass)) as IVsTextManager;
			if (mgr == null)
				return;

			mgr.NavigateToLineAndColumn(buffer, ref logicalView, task.Line, task.Column, task.Line, task.Column);
		}

		/// <summary>
		/// This is the delegate for BuildStartedHandler events.
		/// </summary>
		private void BuildStartedHandler(object sender, BuildStartedEventArgs buildEvent)
		{
			if (LogAtImportance(MessageImportance.Low))
			{
				LogEvent(sender, buildEvent);
			}
			// Remove all errors and warnings since we are rebuilding
			taskProvider.Tasks.Clear();
		}

		/// <summary>
		/// This is the delegate for BuildFinishedHandler events.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="buildEvent"></param>
		private void BuildFinishedHandler(object sender, BuildFinishedEventArgs buildEvent)
		{
			if (LogAtImportance(buildEvent.Succeeded ? MessageImportance.Low :
													   MessageImportance.High))
			{
				if (this.outputWindowPane != null)
					this.outputWindowPane.OutputStringThreadSafe(Environment.NewLine);
				LogEvent(sender, buildEvent);
			}
		}


		/// <summary>
		/// This is the delegate for ProjectStartedHandler events.
		/// </summary>
		private void ProjectStartedHandler(object sender, ProjectStartedEventArgs buildEvent)
		{
			if (LogAtImportance(MessageImportance.Low))
			{
				LogEvent(sender, buildEvent);
			}
		}

		/// <summary>
		/// This is the delegate for ProjectFinishedHandler events.
		/// </summary>
		private void ProjectFinishedHandler(object sender, ProjectFinishedEventArgs buildEvent)
		{
			if (LogAtImportance(buildEvent.Succeeded ? MessageImportance.Low
													 : MessageImportance.High))
			{
				LogEvent(sender, buildEvent);
			}
		}

		/// <summary>
		/// This is the delegate for TargetStartedHandler events.
		/// </summary>
		private void TargetStartedHandler(object sender, TargetStartedEventArgs buildEvent)
		{
			if (LogAtImportance(MessageImportance.Normal))
			{
				LogEvent(sender, buildEvent);
			}
			++this.currentIndent;
		}


		/// <summary>
		/// This is the delegate for TargetFinishedHandler events.
		/// </summary>
		private void TargetFinishedHandler(object sender, TargetFinishedEventArgs buildEvent)
		{
			--this.currentIndent;
			if ((isLogTaskDone) &&
				LogAtImportance(buildEvent.Succeeded ? MessageImportance.Low
													 : MessageImportance.High))
			{
				LogEvent(sender, buildEvent);
			}
		}


		/// <summary>
		/// This is the delegate for TaskStartedHandler events.
		/// </summary>
		private void TaskStartedHandler(object sender, TaskStartedEventArgs buildEvent)
		{
			if (LogAtImportance(MessageImportance.Normal))
			{
				LogEvent(sender, buildEvent);
			}
			++this.currentIndent;
		}


		/// <summary>
		/// This is the delegate for TaskFinishedHandler events.
		/// </summary>
		private void TaskFinishedHandler(object sender, TaskFinishedEventArgs buildEvent)
		{
			--this.currentIndent;
			if ((isLogTaskDone) &&
				LogAtImportance(buildEvent.Succeeded ? MessageImportance.Normal
													 : MessageImportance.High))
			{
				LogEvent(sender, buildEvent);
			}
		}


		/// <summary>
		/// This is the delegate for CustomHandler events.
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="buildEvent"></param>
		private void CustomHandler(object sender, CustomBuildEventArgs buildEvent)
		{
			LogEvent(sender, buildEvent);
		}

		#endregion

		#region helpers
		/// <summary>
		/// This method takes a MessageImportance and returns true if messages
		/// at importance i should be loggeed.  Otherwise return false.
		/// </summary>
		private bool LogAtImportance(MessageImportance importance)
		{
			// If importance is too low for current settings, ignore the event
			bool logIt = false;

			this.SetVerbosity();

			switch (this.Verbosity)
			{
				case LoggerVerbosity.Quiet:
					logIt = false;
					break;
				case LoggerVerbosity.Minimal:
					logIt = (importance == MessageImportance.High);
					break;
				case LoggerVerbosity.Normal:
				// Falling through...
				case LoggerVerbosity.Detailed:
					logIt = (importance != MessageImportance.Low);
					break;
				case LoggerVerbosity.Diagnostic:
					logIt = true;
					break;
				default:
					Debug.Fail("Unknown Verbosity level. Ignoring will cause everything to be logged");
					break;
			}

			return logIt;
		}

		/// <summary>
		/// This is the method that does the main work of logging an event
		/// when one is sent to this logger.
		/// </summary>
		private void LogEvent(object sender, BuildEventArgs buildEvent)
		{
			// Fill in the Message text
			if (OutputWindowPane != null && !String.IsNullOrEmpty(buildEvent.Message))
			{
				StringBuilder msg = new StringBuilder(this.currentIndent + buildEvent.Message.Length + 1);
				if (this.currentIndent > 0)
				{
					msg.Append('\t', this.currentIndent);
				}
				msg.AppendLine(buildEvent.Message);
				this.OutputWindowPane.OutputStringThreadSafe(msg.ToString());
			}
		}

		/// <summary>
		/// This is called when the build complete.
		/// </summary>
		private void ShutdownLogger()
		{
		}


		/// <summary>
		/// Format error messages for the task list
		/// </summary>
		/// <param name="e"></param>
		/// <returns></returns>
		private string GetFormattedErrorMessage(CompilerError e)
		{
			if (e == null) return String.Empty;

			string errCode = (e.IsWarning) ? this.warningString : this.errorString;
			StringBuilder fileRef = new StringBuilder();

			if (!string.IsNullOrEmpty(e.FileName))
			{
				fileRef.AppendFormat(CultureInfo.CurrentUICulture, "{0}({1},{2}):",
                                        e.FileName, e.Line, e.Column);
			}
			fileRef.AppendFormat(CultureInfo.CurrentUICulture, " {0} {1}: {2}", errCode, e.ErrorNumber, e.ErrorText);

			return fileRef.ToString();
		}

		/// <summary>
		/// Formats the message that is to be output.
		/// </summary>
		/// <param name="message">The message string.</param>
		/// <returns>The new message</returns>
		private string FormatMessage(string message)
		{
			if (string.IsNullOrEmpty(message))
			{
				return Environment.NewLine;
			}

			StringBuilder sb = new StringBuilder(message.Length + Environment.NewLine.Length);

			sb.AppendLine(message);
			return sb.ToString();
		}

		/// <summary>
		/// Sets the verbosity level.
		/// </summary>
		private void SetVerbosity()
		{
			// TODO: This should be replaced when we have a version that supports automation.

			string verbosityKey = String.Format(CultureInfo.InvariantCulture, @"{0}\{1}", BuildVerbosityRegistryRoot, buildVerbosityRegistrySubKey);
			using (RegistryKey subKey = Registry.CurrentUser.OpenSubKey(verbosityKey))
			{
				if (subKey != null)
				{
					object valueAsObject = subKey.GetValue(buildVerbosityRegistryKey);
					if (valueAsObject != null)
					{
						this.Verbosity = (LoggerVerbosity)((int)valueAsObject);
					}
				}
			}

			// TODO: Continue this code to get the Verbosity when we have a version that supports automation to get the Verbosity.
			//EnvDTE.DTE dte = this.serviceProvider.GetService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
			//EnvDTE.Properties properties = dte.get_Properties(EnvironmentCategory, ProjectsAndSolutionSubCategory);
		}
		#endregion
	}

}
